package com.dycong.common.network;

import com.dycong.common.common.ApplicationConfig;
import lombok.Data;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.HttpClientUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.cookie.Cookie;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;

import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static org.apache.http.conn.ssl.SSLConnectionSocketFactory.getDefaultHostnameVerifier;

//TODO HttpClient 本身就是一个连接线程池，PoolingHttpClientConnectionManager配置线程池

/**
 * Created dycong Duke on 2017/2/20.
 */
public class HttpClientConfiguration {

    static Logger log = Logger.getLogger(HttpClientConfiguration.class);

    private static String CA_PATH;
    private static String CA_PASSWORD;
    private static int maxTotal = 100;
    private static int maxPerRoute = 10;
    private static int timeout = 60 * 1000;
    SSLConnectionSocketFactory sslConnectionSocketFactory;
    PoolingHttpClientConnectionManager poolingHttpClientConnectionManager;
    RequestConfig requestConfig;
    HttpClientContext localContext = HttpClientContext.create();
    ;
    CookieStore cookieStore = new BasicCookieStore();
    HttpClient client;

    final static Map<String, HttpClientConfiguration> instanceMap = new HashMap<>();

    HttpClientConfiguration() {
    }

    public static HttpClientConfiguration getMultiInstance(String caPath, String caPassword, int maxTotal, int maxPerRoute, String proxyHost, int proxyPort, int timeout, List<Cookie> cookies) {
        String key = caPath + caPassword + maxTotal + maxPerRoute + proxyHost + proxyPort + timeout;
        synchronized ((HttpClientConfiguration.class + key).intern()) {
            if (ifEmpty(caPath) & ifEmpty(caPassword)) {
                key = "MultiInstance";
            }
            HttpClientConfiguration httpClientConfiguration = instanceMap.get(key);
            if (httpClientConfiguration == null) {
                httpClientConfiguration = new HttpClientConfiguration();
                httpClientConfiguration.setCertificate(caPath, caPassword)
                        .setConnectionManager(maxTotal, maxPerRoute)
                        .setRequestConfig(proxyHost, proxyPort, timeout);
                if (NoNull(cookies))
                    httpClientConfiguration.setCookie(cookies);
//                if (!NoNull(httpClientConfiguration.sslConnectionSocketFactory))
//                    httpClientConfiguration.sslConnectionSocketFactory= new SSLConnectionSocketFactory(SSLContexts.createDefault(), getDefaultHostnameVerifier());
                httpClientConfiguration.client = HttpClients.custom()
                        .setSSLSocketFactory(httpClientConfiguration.sslConnectionSocketFactory)
                        .setConnectionManager(httpClientConfiguration.poolingHttpClientConnectionManager)
                        .setDefaultRequestConfig(httpClientConfiguration.requestConfig)
                        .evictExpiredConnections()  //驱逐过期连接
                        .evictIdleConnections(5L,TimeUnit.SECONDS)  //驱逐空闲连接
                        .build();
                instanceMap.put(key, httpClientConfiguration);
            }
            return httpClientConfiguration;

        }
    }

    public static HttpClientConfiguration getSslInstance(String caPath, String caPassword) {
        String key = caPath + caPassword;
        synchronized ((HttpClientConfiguration.class + key).intern()) {
            if (ifEmpty(caPath) & ifEmpty(caPassword)) {
                key = "";
            }
            HttpClientConfiguration httpClientConfiguration = instanceMap.get(key);
            if (httpClientConfiguration == null) {
                httpClientConfiguration = new HttpClientConfiguration();
                httpClientConfiguration.setCertificate(caPath, caPassword);
                httpClientConfiguration.client = HttpClients.custom().setSSLSocketFactory(httpClientConfiguration.sslConnectionSocketFactory).build();
                instanceMap.put(key, httpClientConfiguration);
            }
            return httpClientConfiguration;
        }
    }

    public static HttpClientConfiguration getManagerInstance(int maxTotal, int maxPerRoute) {
        String key = maxTotal + maxPerRoute + "";
        synchronized ((HttpClientConfiguration.class + key).intern()) {

            HttpClientConfiguration httpClientConfiguration = instanceMap.get(key);
            if (httpClientConfiguration == null) {
                httpClientConfiguration = new HttpClientConfiguration();
                httpClientConfiguration.setConnectionManager(maxTotal, maxPerRoute);
                httpClientConfiguration.client = HttpClients.custom()
                        .setConnectionManager(httpClientConfiguration.poolingHttpClientConnectionManager)
                        .evictExpiredConnections()  //驱逐过期连接
                        .evictIdleConnections(5L,TimeUnit.SECONDS)  //驱逐空闲连接
                        .build();
                instanceMap.put(key, httpClientConfiguration);
            }
            return httpClientConfiguration;
        }
    }

    public static HttpClientConfiguration getProxyInstance(String proxyHost, int proxyPort) {
        String key = proxyHost + proxyPort;
        synchronized ((HttpClientConfiguration.class + key).intern()) {
            if (proxyHost == null || proxyHost.equals("") || proxyPort == 0) {
                key = "";
            }
            HttpClientConfiguration httpClientConfiguration = instanceMap.get(key);
            if (httpClientConfiguration == null) {
                httpClientConfiguration = new HttpClientConfiguration();
                httpClientConfiguration.setRequestConfig(proxyHost, proxyPort, 0);
                httpClientConfiguration.client = HttpClients.custom().setDefaultRequestConfig(httpClientConfiguration.requestConfig).build();
                instanceMap.put(key, httpClientConfiguration);
            }
            return httpClientConfiguration;
        }
    }
//          BasicClientCookie cookie = new BasicClientCookie(name,value);
//          cookie.setDomain(target.getHostName());
//          cookie.setPath("/");

    HttpClientConfiguration setCookie(List<Cookie> cookies) {
        for (Cookie cookie : cookies)
            this.cookieStore.addCookie(cookie);
        this.localContext.setCookieStore(this.cookieStore);
        return this;        //todo return this之后就可以.set..().set..()。
    }

    HttpClientConfiguration setConnectionManager(int maxTotal, int maxPerRoute) {
        this.maxTotal = maxTotal;
        this.maxPerRoute = maxPerRoute;
        this.poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
        poolingHttpClientConnectionManager.setMaxTotal(maxTotal);
        poolingHttpClientConnectionManager.setDefaultMaxPerRoute(maxPerRoute);
        return this;
    }

    HttpClientConfiguration setSslConnectionManager(int maxTotal, int maxPerRoute, String caPath, String caPassword) {
        setCertificate(caPath, caPassword);
        this.maxTotal = maxTotal;
        this.maxPerRoute = maxPerRoute;
        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.INSTANCE)
                .register("https", this.sslConnectionSocketFactory)
                .build();
        this.poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
        return this;
    }

    HttpClientConfiguration setCertificate(String caPath, String caPassword) {
        try {
            if (ifEmpty(caPath) & ifEmpty(caPassword)) {
                this.CA_PATH = caPath;
                this.CA_PASSWORD = caPassword;
                SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(new File(CA_PATH), CA_PASSWORD.toCharArray(), new TrustSelfSignedStrategy())
                        .build();
                this.sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslcontext);
            }
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("NoSuchAlgorithmException", e);
        } catch (KeyStoreException e) {
            throw new RuntimeException("KeyStoreException", e);
        } catch (CertificateException e) {
            throw new RuntimeException("CertificateException", e);
        } catch (IOException e) {
            throw new RuntimeException("IOException", e);
        } catch (KeyManagementException e) {
            throw new RuntimeException("KeyManagementException", e);
        }
        return this;
    }


    HttpClientConfiguration setRequestConfig(String proxyHost, int proxyPort, int timeout) {
        setRequestConfig1(proxyHost, proxyPort, timeout);
        return this;
    }

    void setRequestConfig1(String proxyHost, int proxyPort, int timeout) {
        if (setRequestConfig2(proxyHost, proxyPort, timeout))
            return;

        log.info("try to set proxy from ApplicationConfig");
        String configHost = ApplicationConfig.get("proxyHost").orElse(null);
        int configPort = ApplicationConfig.getInt("proxyPort").orElse(0);
        if (setRequestConfig2(configHost, configPort, timeout))
            return;

        log.info("set proxy from environment variable");
        String host = System.getProperty("http.proxyHost");
        try {
            int port = Integer.valueOf(System.getProperty("http.proxyPort"));
            setRequestConfig2(host, port, timeout);
        } catch (RuntimeException e) {
            log.info("try to get proxy config from ENV failed. no proxy now");
        }
    }

    boolean setRequestConfig2(String host, int port, int timeout) {
        if (isProxyAvailable(host, port)) {
            log.info("setting proxy config: " + host + ":" + port);
            if (timeout == 0)
                timeout = this.timeout;
            HttpHost proxy = new HttpHost(host, port);
            if (null == this.requestConfig) {
                this.requestConfig = RequestConfig.custom()
                        .setProxy(proxy)
                        .setSocketTimeout(timeout)
                        .setConnectTimeout(timeout)
                        .setConnectionRequestTimeout(timeout).build();
            } else {
                this.requestConfig = RequestConfig.custom().setProxy(proxy).build();
            }
            return true;
        } else {
            log.debug("proxy is not available: " + host + ":" + port);
            return false;
        }
    }

    Result execute(HttpRequestBase request, String encoding) {
        try {
            if (requestConfig != null)
                request.setConfig(requestConfig);
            HttpResponse response = client.execute(request, localContext);
            try {
                Map<String, List<String>> responseMap = Arrays.stream(response.getAllHeaders())
                        .collect(groupingBy(
                                h -> h.getName(),
                                mapping(h -> h.getValue(), toList())
                        ));
                String content = EntityUtils.toString(response.getEntity(), encoding);
                Result result = new Result();
                result.setResponseHeader(responseMap);
                result.setStatusCode(response.getStatusLine().getStatusCode());
                result.setContent(content);
                log.debug("http calling result: " + result);
                return result;
            } finally {
                HttpClientUtils.closeQuietly(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    public Result doGet(String url, String encoding, Map<String, String> header, Map<String, String> body) {
        url = formatGetUrl(url, body, encoding);
        HttpGet get = new HttpGet(url);
        log.debug("doGET: " + url);

        if (header != null)
            header.forEach(get::setHeader);

        return execute(get, encoding);
    }


    public Result doPost(String url, String encoding, Map<String, String> headerMap, Map<String, String> paramMap) {
        HttpPost post = new HttpPost(url);
        if (headerMap == null) {
            headerMap = new HashMap<>();
        }
        headerMap.forEach(post::addHeader);
        if (!headerMap.containsKey("Content-type")) {
            post.addHeader("Content-type", "application/x-www-form-urlencoded;charset=" + encoding);
        }
        if (paramMap != null) {
            List<NameValuePair> params = paramMap.keySet()
                    .stream()
                    .map(key -> new BasicNameValuePair(key, paramMap.get(key)))
                    .collect(Collectors.toList());
            try {
                HttpEntity entity = new UrlEncodedFormEntity(params, encoding);
                post.setEntity(entity);
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        }

        return execute(post, encoding);
    }

    public Result postJson(String url, String encoding, Map<String, String> headerMap, String json) {
        HttpPost post = new HttpPost(url);
        if (headerMap != null) {
            headerMap.forEach(post::setHeader);
        }

        post.setEntity(new StringEntity(json, encoding));
        post.addHeader("content-type", "application/json; charset=" + encoding);

        return execute(post, encoding);
    }

    boolean isProxyAvailable(String host, int port) {
        //TODO
        return host != null && !host.equals("") && port > 0 && port < (1 << 16);
    }

    public static boolean ifEmpty(String str) {
        if (str != null && !"".equals(str.trim())) {
            return true;
        }
        return false;
    }

    public static boolean NoNull(Object o) {
        if (null != o) {
            return true;
        }
        return false;
    }

    @Data
    class Result {
        String content;
        int statusCode;
        Map<String, List<String>> responseHeader;
    }

    public String formatGetUrl(String originalURl, Map<String, String> paramMap, String encoding) {
        if (paramMap == null)
            return originalURl;
        List<? extends NameValuePair> list =
                paramMap.entrySet().stream().map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue())).collect(toList());
        if (originalURl.contains("?")) {
            return originalURl + "&" + URLEncodedUtils.format(list, encoding);
        } else {
            return originalURl + "?" + URLEncodedUtils.format(list, encoding);
        }
    }

}
